/*
 * Copyright 2011-17 Fraunhofer ISE, energy & meteo Systems GmbH and other contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package org.openmuc.openiec61850;

import java.util.Calendar;
import java.util.Date;

import org.openmuc.jasn1.ber.types.BerNull;
import org.openmuc.openiec61850.internal.mms.asn1.Data;
import org.openmuc.openiec61850.internal.mms.asn1.TypeDescription;
import org.openmuc.openiec61850.internal.mms.asn1.UtcTime;

public final class BdaTimestamp extends BasicDataAttribute {

    private byte[] value;

    public BdaTimestamp(ObjectReference objectReference, Fc fc, String sAddr, boolean dchg, boolean dupd) {
        super(objectReference, fc, sAddr, dchg, dupd);
        basicType = BdaType.TIMESTAMP;
        setDefault();
    }

    /**
     * The SecondSinceEpoch shall be the interval in seconds continuously counted from the epoch 1970-01-01 00:00:00 UTC
     */

    /**
     * Returns the value as the number of seconds since epoch 1970-01-01 00:00:00 UTC
     * 
     * @return the number of seconds since epoch 1970-01-01 00:00:00 UTC
     */
    private long getSecondsSinceEpoch() {
        return ((0xffL & value[0]) << 24 | (0xffL & value[1]) << 16 | (0xffL & value[2]) << 8 | (0xffL & value[3]));
    }

    /**
     * The attribute FractionOfSecond shall be the fraction of the current second when the value of the TimeStamp has
     * been determined. The fraction of second shall be calculated as
     * <code>(SUM from I = 0 to 23 of bi*2**–(I+1) s).</code>
     * 
     * NOTE 1 The resolution is the smallest unit by which the time stamp is updated. The 24 bits of the integer
     * provides 1 out of 16777216 counts as the smallest unit; calculated by 1/2**24 which equals approximately 60 ns.
     * 
     * NOTE 2 The resolution of a time stamp may be 1/2**1 (= 0,5 s) if only the first bit is used; or may be 1/2**2 (=
     * 0,25 s) if the first two bits are used; or may be approximately 60 ns if all 24 bits are used. The resolution
     * provided by an IED is outside the scope of this standard.
     * 
     * @return the fraction of seconds
     */
    private int getFractionOfSecond() {
        return ((0xff & value[4]) << 16 | (0xff & value[5]) << 8 | (0xff & value[6]));
    }

    public void setDate(Date date) {
        if (value == null) {
            value = new byte[8];
        }

        int secondsSinceEpoch = (int) (date.getTime() / 1000L);
        int fractionOfSecond = (int) ((date.getTime() % 1000L) / 1000.0 * (1 << 24));

        // 0x8a = time accuracy of 10 and LeapSecondsKnown = true, ClockFailure
        // = false, ClockNotSynchronized = false
        value = new byte[] { (byte) ((secondsSinceEpoch >> 24) & 0xff), (byte) ((secondsSinceEpoch >> 16) & 0xff),
                (byte) ((secondsSinceEpoch >> 8) & 0xff), (byte) (secondsSinceEpoch & 0xff),
                (byte) ((fractionOfSecond >> 16) & 0xff), (byte) ((fractionOfSecond >> 8) & 0xff),
                (byte) (fractionOfSecond & 0xff), (byte) 0x8a };

    }

    public void setDate(Date date, boolean leapSecondsKnown, boolean clockFailure, boolean clockNotSynchronized,
            int timeAccuracy) {
        if (value == null) {
            value = new byte[8];
        }

        int secondsSinceEpoch = (int) (date.getTime() / 1000L);
        int fractionOfSecond = (int) ((date.getTime() % 1000L) / 1000.0 * (1 << 24));

        int timeQuality = timeAccuracy & 0x1f;
        if (leapSecondsKnown) {
            timeQuality = timeQuality | 0x80;
        }
        if (clockFailure) {
            timeQuality = timeQuality | 0x40;
        }
        if (clockNotSynchronized) {
            timeQuality = timeQuality | 0x20;
        }

        value = new byte[] { (byte) ((secondsSinceEpoch >> 24) & 0xff), (byte) ((secondsSinceEpoch >> 16) & 0xff),
                (byte) ((secondsSinceEpoch >> 8) & 0xff), (byte) (secondsSinceEpoch & 0xff),
                (byte) ((fractionOfSecond >> 16) & 0xff), (byte) ((fractionOfSecond >> 8) & 0xff),
                (byte) (fractionOfSecond & 0xff), (byte) timeQuality };

    }

    public void setValue(byte[] value) {
        if (value == null) {
            this.value = new byte[8];
        }
        this.value = value;
    }

    @Override
    void setValueFrom(BasicDataAttribute bda) {
        byte[] srcValue = ((BdaTimestamp) bda).getValue();
        if (value.length != srcValue.length) {
            value = new byte[srcValue.length];
        }
        System.arraycopy(srcValue, 0, value, 0, srcValue.length);
    }

    public Date getDate() {
        if (value == null || value.length == 0) {
            return null;
        }
        long time = getSecondsSinceEpoch() * 1000L + (long) (((float) getFractionOfSecond()) / (1 << 24) * 1000 + 0.5);
        return new Date(time);
    }

    public byte[] getValue() {
        return value;
    }

    /**
     * The value TRUE of the attribute LeapSecondsKnown shall indicate that the value for SecondSinceEpoch takes into
     * account all leap seconds occurred. If it is FALSE then the value does not take into account the leap seconds that
     * occurred before the initialization of the time source of the device.
     * 
     * Java {@link Date} and {@link Calendar} objects do handle leap seconds, so this is usually true.
     * 
     * @return TRUE of the attribute LeapSecondsKnown shall indicate that the value for SecondSinceEpoch takes into
     *         account all leap seconds occurred
     */
    public boolean getLeapSecondsKnown() {
        return ((value[7] & 0x80) != 0);
    }

    /**
     * The attribute clockFailure shall indicate that the time source of the sending device is unreliable. The value of
     * the TimeStamp shall be ignored.
     * 
     * @return true if the time source of the sending device is unreliable
     */
    public boolean getClockFailure() {
        return ((value[7] & 0x40) != 0);
    }

    /**
     * The attribute clockNotSynchronized shall indicate that the time source of the sending device is not synchronized
     * with the external UTC time.
     * 
     * @return true if the time source of the sending device is not synchronized
     */
    public boolean getClockNotSynchronized() {
        return ((value[7] & 0x20) != 0);
    }

    /**
     * The attribute TimeAccuracy shall represent the time accuracy class of the time source of the sending device
     * relative to the external UTC time. The timeAccuracy classes shall represent the number of significant bits in the
     * FractionOfSecond
     * 
     * If the time is set via Java {@link Date} objects, the accuracy is 1 ms, that is a timeAccuracy value of 10.
     * 
     * @return the time accuracy
     */
    public int getTimeAccuracy() {
        return ((value[7] & 0x1f));
    }

    /**
     * Sets Timestamp the empty byte array (indicating an invalid Timestamp)
     */
    @Override
    public void setDefault() {
        value = new byte[8];
    }

    /**
     * Sets Timestamp to current time
     */
    public void setCurrentTime() {
        setDate(new Date());
    }

    @Override
    public BdaTimestamp copy() {
        BdaTimestamp copy = new BdaTimestamp(objectReference, fc, sAddr, dchg, dupd);
        byte[] valueCopy = new byte[value.length];
        System.arraycopy(value, 0, valueCopy, 0, value.length);
        copy.setValue(valueCopy);
        if (mirror == null) {
            copy.mirror = this;
        }
        else {
            copy.mirror = mirror;
        }
        return copy;
    }

    @Override
    Data getMmsDataObj() {
        Data data = new Data();
        data.setUtcTime(new UtcTime(value));
        return data;
    }

    @Override
    void setValueFromMmsDataObj(Data data) throws ServiceError {
        if (data.getUtcTime() == null) {
            throw new ServiceError(ServiceError.TYPE_CONFLICT, "expected type: utc_time/timestamp");
        }
        value = data.getUtcTime().value;
    }

    @Override
    TypeDescription getMmsTypeSpec() {
        TypeDescription typeDescription = new TypeDescription();
        typeDescription.setUtcTime(new BerNull());
        return typeDescription;
    }

    @Override
    public String toString() {
        return getReference().toString() + ": " + getDate();
    }

}
